# Copyright (c) 2016, Neville-Neil Consulting
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are
# met:
#
# Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
#
# Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
#
# Neither the name of Neville-Neil Consulting nor the names of its 
# contributors may be used to endorse or promote products derived from 
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#
#' @author George V. Neville-Neil, \email{gnn@neville-neil.com}
#
# Description: A set of libraries to process PTP log files.  Used by
# various command line tools in this directory.
#

require(zoo) # Time series objects

# Functions for working with PTP log output

#' Read a PTP log file
#' 
#' @param file A properly formatted PTP log file generated by the daemon
#'
#' @return The raw data frame, merged timeseries (offset, delay, m->s, s->)
#'          offset time series, delay time series,
#'          master.to.slave time series, and slave.to.master time series
#'
#' @export
ptpLogRead <- function(file) {

    # Attempt to read log files from version 2.3 first.
    try( {
      data = read.table(file, fill=TRUE, sep=",",
        col.names=c("timestamp", "state", "clockID", "delay", "offset", "slave.to.master", "master.to.slave", "drift", "packet", "sequence", "one.way.delay.mean", "one.way.delay.dev", "offset.mean", "offset.dev", "drift.mean", "drift.dev", "delay.ms", "delay.sm"),
        colClasses=c("timestamp"="POSIXct"),
        blank.lines.skip=TRUE, header=FALSE, skip=100)
    } ,silent=TRUE)
    
    if (is.na(data$packet[1])) {
      print(paste("File is not new style, trying old style.", file))
      try( {
        data = read.table(file, fill=TRUE, sep=",",
          col.names=c("timestamp", "state", "clockID", "delay", "offset", "master.to.slave", "slave.to.master", "steering", "drift", "packet"),
          colClasses=c("timestamp"="POSIXct"),
          blank.lines.skip=TRUE, header=FALSE, skip=100)
      }, silent=TRUE)
      
    }

    if (is.na(data$packet[1])) {
      print(paste("File is not old style, trying SolarFlare style.", file))
    tryCatch( {
      data = read.table(file, fill=FALSE, sep=",",
        col.names=c("timestamp", "offset", "freq-adj", "in-sync", "delay", "clockID"), 
        colClasses=c("timestamp"="POSIXct"),
        blank.lines.skip=TRUE, header=FALSE, skip=100)
    }, finally = { # Nothing to print here
      
    } )
  }

    offset = zoo(data$offset, data$timestamp)
    delay = zoo(data$delay, data$timestamp)
    master.to.slave = zoo(data$master.to.slave, data$timestamp)
    slave.to.master = zoo(data$slave.to.master, data$timestamp)
    ts = merge(offset, delay, master.to.slave, slave.to.master)

    return (list(data=data, ts=ts, offset=offset, delay=delay,
                 master.to.slave=master.to.slave,
                 slave.to.master=slave.to.master))
}

#' Graphs a dataframe returned by a call to ptplog
#'
#' @param logframe A data frame returned by ptpLogRead
#' @param value Graph only a specific value rather than the four major ones.
#' @param output An optional file name to place a PNG of the graph into
#'
#' @export
ptpGraph <- function(logframe, value, start, end, output) {
    if (!missing(output))
        png(filename=output, height=960, width=1280, bg="white")
    if (class(logframe) == "zoo") {
        if (!missing(start))
            logframe = logframe[index(logframe) > as.POSIXct(start)]
        if (!missing(end))
            logframe = logframe[index(logframe) < as.POSIXct(end)]
        if (!missing(start) && !missing(end))
            logframe = logframe[index(logframe) > as.POSIXct(start) &
                index(logframe) < as.POSIXct(end)]
        plot(logframe, type="p", cex=.1, main="PTP Log Graph", xlab="Time", mar=c(1, 5.1, 1, 5.1))
        if (!missing(output))
            dev.off()
        return(NULL)
    }
    if (missing(value)) {
        ymin = min(min(logframe$offset, na.rm=TRUE), min(logframe$delay, na.rm=TRUE),
            min(logframe$master.to.slave, na.rm=TRUE),
            min(logframe$slave.to.master, na.rm=TRUE))
        ymax = max(max(logframe$offset, na.rm=TRUE), max(logframe$delay, na.rm=TRUE),
            max(logframe$master.to.slave, na.rm=TRUE),
            max(logframe$slave.to.master, na.rm=TRUE))
        plot(logframe$delay, y=NULL, xaxt = "n" ,type="n", ylim=range(ymin, ymax),
             main="PTP Results", xlab="Time", ylab="Nanoseconds")
        legend("topright",
               c("Delay", "Offset", "M->S", "S->M"), col=c("orange", "blue", "red", "green"), pch=21:24)
        points(logframe$offset, y=NULL, cex=.1, col="blue", pch=21)
        points(logframe$master.to.slave, y=NULL, cex=.1, col="red", pch=22)
        points(logframe$slave.to.master, y=NULL, cex=.1, col="green", pch=23)
        points(logframe$delay, y=NULL, cex=.1, col="orange", pch=24)
    } else {
        ymin = min(logframe[[value]], na.rm=TRUE)
        ymax = max(logframe[[value]], na.rm=TRUE)
        plot(logframe[[value]], y=NULL, xaxt = "n" ,type="n",
             ylim=range(ymin, ymax),
             main="PTP Results", xlab="Time", ylab="Nanoseconds")
        points(logframe[[value]], y=NULL, cex=.1, col="red", pch=21)
    }
    if (!missing(output))
        dev.off()
}
    
#' Draw a simple histogram of some one variable in our log
#'
#' @examples ptpHistogram(foo$log$offset)
#'
#' @export
ptpHistogram <- function(log, start, end, output) {
    if (!missing(output))
        png(filename=output, height=960, width=1280, bg="white")
    if (!missing(start))
        log = log[index(log) > as.POSIXct(start)]
    if (!missing(end))
        log = log[index(log) < as.POSIXct(end)]
    if (!missing(start) && !missing(end))
        log = log[index(log) > as.POSIXct(start) &
            index(log) < as.POSIXct(end)]
    left = min(log)
    right = max(log)
    height = max(length(log)) * 0.69
    hist (log, xlim=c(left, right), ylim=c(0,height), breaks=20,
          col=rgb(1.0, 0, 0, 0.5))
    if (!missing(output))
        dev.off()
}

#' Compare two data sets to each other using a histogram
#'
#' examples ptpHistogramCompare(foo$log$offset, bar$log$offset)
#'
#' @export
ptpHistogramCompare <- function(loga, logb, starta, enda, startb, endb,
                                output) {
    if (!missing(output))
        png(filename=output, height=960, width=1280, bg="white")
    if (!missing(starta) && !missing(startb)) {
        loga = loga[index(loga) > as.POSIXct(starta)]
        logb = logb[index(logb) > as.POSIXct(startb)]
    }
    if (!missing(enda) && !missing(endb)) {
        loga = loga[index(loga) < as.POSIXct(enda)]
        logb = logb[index(logb) < as.POSIXct(endb)]
    }
    if (!missing(starta) && !missing(enda) &&
        !missing(startb) && !missing(endb)) {
        loga = loga[index(loga) > as.POSIXct(starta) &
            index(loga) < as.POSIXct(enda)]
        logb = logb[index(logb) > as.POSIXct(startb) &
            index(logb) < as.POSIXct(endb)]
    }
    left = min(min(loga), min(logb))
    right = max(max(loga), max(logb))
    height = max(length(loga), length(logb)) * 0.69
    hist (loga, xlim=c(left, right), ylim=c(0,height), breaks=20,
          col=rgb(1.0, 0, 0, 0.25))
    hist (logb, xlim=c(left, right), ylim=c(0,height), breaks=20,
          col=rgb(0, 0, 1.0, 0.25), add=TRUE)
    legend("topright",
           c("A", "B"),
           col=c(rgb(1.0, 0, 0, 1), rgb(0, 0, 1.0, 1)), pch=21:22)
    if (!missing(output))
        dev.off()
}

#' Compare two log files on a graph
#'
#' @param loga data frame read using ptplogRead
#' @param logb data frame read using ptplogRead
#' @param value column of the log file to compare
#'          e.g. offset, delay, slave.to.master, master.to.slave
#' @param output an output file name for a PNG image
#'
#' @export
ptpCompare <-function(loga, logb, value, start, end, output) {
    if (!missing(output))
        png(filename=output, height=960, width=1280, bg="white")
    if (!missing(start)) {
        loga = loga[index(loga) > as.POSIXct(start)]
        logb = logb[index(logb) > as.POSIXct(start)]
    }
    if (!missing(end)) {
        loga = loga[index(loga) < as.POSIXct(end)]
        logb = logb[index(logb) < as.POSIXct(end)]
    }
    if (!missing(start) && !missing(end)) {
        loga = loga[index(loga) > as.POSIXct(start) &
            index(loga) < as.POSIXct(end)]
        logb = logb[index(logb) > as.POSIXct(start) &
            index(logb) < as.POSIXct(end)]
    }
    # If the caller does not specify which value to compare, compare them all
    if (missing (value)) {
        ymin = min(min(loga$offset, na.rm=TRUE), min(loga$delay, na.rm=TRUE),
            min(loga$master.to.slave, na.rm=TRUE),
            min(loga$slave.to.master, na.rm=TRUE))
        ymax = max(max(loga$offset, na.rm=TRUE), max(loga$delay, na.rm=TRUE),
            max(loga$master.to.slave, na.rm=TRUE),
            max(loga$slave.to.master, na.rm=TRUE))

        plot(loga$delay, y=NULL, xaxt = "n" ,type="n", ylim=range(ymin, ymax),
             main="PTP Results", xlab="Time", ylab="Nanoseconds")
        points(loga$delay, y=NULL, cex=.5, col="black", pch=24)
        points(loga$offset, y=NULL, cex=.5, col="blue", pch=21)
        points(loga$master.to.slave, y=NULL, cex=.5, col="purple", pch=22)
        points(loga$slave.to.master, y=NULL, cex=.5, col="green", pch=23)

        points(logb$delay, y=NULL, cex=.5, col="black", pch=24)
        points(logb$offset, y=NULL, cex=.5, col="deeppink", pch=21)
        points(logb$master.to.slave, y=NULL, cex=.5, col="blue", pch=22)
        points(logb$slave.to.master, y=NULL, cex=.5, col="red", pch=23)
        legend("topright",
               c("Delay1", "Offset1", "M->S_1", "S->M_1", "Delay2", "Offset2", "M->S_2", "S->M_w"), col=c("black", "blue", "purple", "green", "black", "deeppink", "blue", "red"), pch=21:24)
    } else {
        ymin = min(min(loga[[value]], na.rm=TRUE), min(logb[[value]], na.rm=TRUE))
        ymax = max(max(loga[[value]], na.rm=TRUE), max(logb[[value]], na.rm=TRUE))
        plot(loga[[value]], y=NULL, xaxt = "n" ,type="n", ylim=range(ymin, ymax),
             main="PTP Results", xlab="Time", ylab="Nanoseconds")
        points(loga[[value]], y=NULL, cex=.5, col="red", pch=21)

        points(logb[[value]], y=NULL, cex=.5, col="blue", pch=22)
        legend("topright",
               c(paste(value,"1"), paste(value, "2")), col=c("red", "blue"), pch=21:22)
    }
    if (!missing(output))
        dev.off()
}

#' Functions for working with PTPd quality files
#'
#' @param file quality file to read
#' @return a data frame based on a PTPd quality file.
#' 
ptpQualityRead <- function(file) {
    read.table(file, fill=FALSE, sep=" ",
               col.names=c("packet", "timestamp"),
               colClasses=c("packet"="numeric"),
               blank.lines.skip=TRUE, header=FALSE)
}

#' Graph quality data
#'
#' @param logA data from host A
#' @param logB data from host B
#' @param output optional output file for the graph
#' @return None
#' 
#'
#' @export
ptpQualityGraph <- function(logA, logB, output) {
    if (!missing(output))
        png(filename=output, height=960, width=1280, bg="white")
    quality = merge(logA, logB, by="packet")
    difference = zoo(quality$timestamp.x - quality$timestamp.y, quality$packet)
    plot(difference, type="p", cex=.1, ylab="nanoseconds",
         xlab="Sequence Number")
    if (!missing(output))
            dev.off()
    return(difference)
}

#' Compare two quality graphs
#'
#' @param diffA difference A
#' @param diffB difference B
#' @param output optional output file for the graph
#' @return None
#' 
#'
#' @export
ptpQualityGraphCompare <- function(diffA, diffB, output) {
    if (!missing(output))
        png(filename=output, height=960, width=1280, bg="white")
    plot(diffA, type = "p", ylab="nanoseconds", xlab="Sequence Number", cex=.1, col="red")
    points(diffB, ylab="nanoseconds", xlab="Sequence Number", cex=.1, col="green")
    legend("topright",
           c("1", "2"), col=c("red", "green"), pch=21:22)
    if (!missing(output))
        dev.off()
}

#' Functions for deriving various statistics over a PTP log
#'
#' @param log dataframe returned by a ptpLogRead() call
#' @param start start time in strftime format
#' @param end end time in strftime format
#' @return None
#' 
#'
#' @export
ptpStats <- function(log, start, end) {
    cat("Measurements: ", length(log$offset))
    cat("\nOffset",
        "\nmin:", min(log$offset, na.rm=TRUE),
        " max: ", max(log$offset, na.rm=TRUE),
        " median: ", median(log$offset, na.rm=TRUE),
        " mean: ", mean(log$offset, na.rm=TRUE),
        "\nstd dev: ", sd(log$offset, na.rm=TRUE),
        " variance: ", var(log$offset, na.rm=TRUE), "\n")
    cat("Delay",
        "\nmin:", min(log$delay, na.rm=TRUE),
        " max: ", max(log$delay, na.rm=TRUE),
        " median: ", median(log$delay, na.rm=TRUE),
        " mean: ", mean(log$delay, na.rm=TRUE),
        "\nstd dev: ", sd(log$delay, na.rm=TRUE),
        " variance: ", var(log$delay, na.rm=TRUE), "\n")
    cat("Master -> Slave",
        "\nmin:", min(log$master.to.slave, na.rm=TRUE),
        " max: ", max(log$master.to.slave, na.rm=TRUE),
        " median: ", median(log$master.to.slave, na.rm=TRUE),
        " mean: ", mean(log$master.to.slave, na.rm=TRUE),
        "\nstd dev: ", sd(log$master.to.slave, na.rm=TRUE),
        " variance: ", var(log$master.to.slave, na.rm=TRUE), "\n")
    cat("Slave -> Master",
        "\nmin:", min(log$slave.to.master, na.rm=TRUE),
        " max: ", max(log$slave.to.master, na.rm=TRUE),
        " median: ", median(log$slave.to.master, na.rm=TRUE),
        " mean: ", mean(log$slave.to.master, na.rm=TRUE),
        "\nstd dev: ", sd(log$slave.to.master, na.rm=TRUE),
        " variance: ", var(log$slave.to.master, na.rm=TRUE), "\n")
}

#' Statistics on offset measurements
#'
#' @param log dataframe returend by a ptpLogRead() call
#' @param start start time in strftime format
#' @param end end time in strftime format
#' @param boundary nanoseconds offset boundary
#'
#' @export
ptpOffsetStats <- function(log, start, end, boundary = 1000) {
    cat("Measurements: ", length(log$offset))
    cat("\nOffset",
        "\nmin:", min(log$offset, na.rm=TRUE),
        " max: ", max(log$offset, na.rm=TRUE),
        " median: ", median(log$offset, na.rm=TRUE),
        " mean: ", mean(log$offset, na.rm=TRUE),
        "\nstd dev: ", sd(log$offset, na.rm=TRUE),
        " variance: ", var(log$offset, na.rm=TRUE), "\n")
    cat("\nOutside Boundary: ", sum(log$offset > boundary) + sum(log$offset < -boundary),
        "Percentage: ", (sum(log$offset > boundary) + sum(log$offset < -boundary)) / length(log$offset), "\n")
}

#' Statistics on quality measurements
#'
#' @param difference dataframes returend by a ptpQualtiyRead() calls
#'
#'
#' @export
ptpQualityStats <- function(difference) {
    cat("\nmin:", min(difference, na.rm=TRUE),
        " max: ", max(difference, na.rm=TRUE),
        " median: ", median(difference, na.rm=TRUE),
        " mean: ", mean(difference, na.rm=TRUE),
        "\nstd dev: ", sd(difference, na.rm=TRUE),
        " variance: ", var(difference, na.rm=TRUE), "\n")
}
